﻿/**
 * Copyright (C) 2012 - present by Privosoft LLC. 
 * 
 * Please see distribution for license.
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using LumenWorks.Framework.IO.Csv;
using System.IO;
using System.Data;

namespace Privosoft.DataFrame
{
    public class FileReader
    {
        private static IFormatProvider culture = new System.Globalization.CultureInfo("en-US", true);


        private static DataTable ProcessRecords(CsvReader csv, int maxRows)
        {
            DataTable table = new DataTable();
            Type stringType = typeof(string);
            string stringTypeString = stringType.ToString();

            // First record: column headers
            if (csv.ReadNextRecord())
            {
                for (int i = 0; i < csv.FieldCount; i++)
                {
                    DataColumn column = new DataColumn();
                    string[] part = csv[i].Split(':');
                    try
                    {
                        column.DataType = Type.GetType(
                                (part.Length == 2 ? part[1] : stringTypeString));
                    }
                    catch (ArgumentException ex)
                    { // add column name and bad data type
                        throw new ArgumentException(part[0] + " " + ex.Message + ": " + part[1]);
                    }
                    column.ColumnName = part[0];
                    table.Columns.Add(column);
                }
            }
            else { throw new FormatException("no header row found"); }
            int rowsCount = 0;
            while (csv.ReadNextRecord())
            {
                if (maxRows >  0 && rowsCount++ > maxRows) break;
                DataRow row = table.NewRow();
                for (int i = 0; i < csv.FieldCount; i++)
                {
                    if (table.Columns[i].GetType().Equals(stringType)
                        || csv[i].Length > 0)
                    {
                        row[i] = csv[i];
                    }
                }
                table.Rows.Add(row);
            }
            return table;
        }

        public static DataTable ReadFile(string fileName, int maxRows)
        {
            DataTable table = null;
            using(CsvReader csv = new CsvReader(new StreamReader(fileName), false))
            {
            
                try { table = ProcessRecords(csv, maxRows); }
                catch (MalformedCsvException ex)
                {
                    throw new IOException(ex.Message);
                }
                finally
                {
                    csv.Dispose(); // file stays tied up without this!
                }
            }
            return table;        
        }

        public static DataTable LoadCsv(string fileName, SchemaInfo schema, int dataFrameId, int maxRows, ImportStatsInfo stats)
        {
            using (var csv = new CsvReader(new StreamReader(fileName), true))
            {
                csv.MissingFieldAction = MissingFieldAction.ReplaceByNull;
                int fieldCount = csv.FieldCount;

                string[] headers = csv.GetFieldHeaders();
                #region Analyze header
                var mapping = new Dictionary<string, Column>();
                foreach (Column c in schema.Columns)
                {
                    bool exist = false;
                    foreach (string header in headers)
                    {
                        if (c.Name.Equals(header, StringComparison.InvariantCultureIgnoreCase))
                        {
                            exist = true;
                            mapping.Add(header, c);
                            break;
                        }
                    }
                    if (!exist)
                    {
                        string msg = String.Format("Field {0} does not match the file structure", c.Name); 

                        // if the column is marked as required and it is missing - then throw an exception
                        var level = c.IsRequired ? LogLevels.FATAL : LogLevels.WARN;
                        Logger.Log(level, 0, msg, c);
                        stats.Errors.Add(new ErrorInfo(0, c, msg, level));

                        if (c.IsRequired)
                            throw new LoaderException(msg);

                    }
                }
                if (mapping.Count == 0)
                {
                    string msg = String.Format("Scheme {0} does not match the file structure", schema.Name);
                    stats.Errors.Add(new ErrorInfo(0, null, msg, LogLevels.FATAL));
                    //stats.RowsImportedCount = 0;
                    throw new LoaderException(msg);
                }
                #endregion
                
                #region Setup Destination Table
                DataTable dt = new DataTable();
                dt.Columns.Add("StringValue", typeof(string));
                dt.Columns.Add("DoubleValue", typeof(decimal));
                dt.Columns.Add("LongValue", typeof(long));
                dt.Columns.Add("DateTimeValue", typeof(DateTime));
                dt.Columns.Add("BooleanValue", typeof(bool));
                dt.Columns.Add("ColumnId", typeof(long));
                dt.Columns.Add("DataFrameId", typeof(long));
                dt.Columns.Add("RowIndex", typeof(long));
                #endregion
                int rowIdx = 0;
                while (csv.ReadNextRecord() && (maxRows > 0 ? rowIdx < maxRows : true))
                {
                    for (int i = 0; i < fieldCount; i++)
                    {
                        if (mapping.ContainsKey(headers[i]))
                        {
                            Column column = null;
                            try
                            {
                                #region COLUMN PROCESSING
                                column = mapping[headers[i]];
                                DataRow row = dt.NewRow();
                                object oVal = csv[i];
                                string fieldName = "";
                                switch (column.DataType.ToLower())
                                {
                                    case "string":
                                        fieldName = "String";
                                        break;
                                    case "double":
                                        fieldName = "Double";
                                        if ((oVal != null) && (oVal.ToString().Length > 0))
                                        {
                                            decimal dec;
                                            if (!Decimal.TryParse(oVal.ToString(), out dec)
                                                && !Decimal.TryParse(oVal.ToString().Replace('.', ','), out dec))
                                            {
                                                oVal = DBNull.Value;
                                            }
                                            else
                                            {
                                                oVal = dec;
                                            }                                            
                                        }
                                        else
                                        {
                                            oVal = DBNull.Value;
                                        }
                                        break;
                                    case "long":
                                        fieldName = "Long";
                                        long val;
                                        if (oVal == null
                                            || string.IsNullOrEmpty(oVal.ToString())
                                            || !long.TryParse(oVal.ToString(), out val))
                                        {
                                            oVal = DBNull.Value;
                                        }
                                        else
                                        {
                                            oVal = val;
                                        }
                                        break;
                                    case "datetime":
                                        fieldName = "DateTime";
                                        if (oVal != null)
                                        {
                                            oVal = Convert.ToDateTime(oVal, culture).ToString("yyyy-MM-dd");
                                        }
                                        break;
                                    case "boolean":
                                        fieldName = "Boolean";
                                        break;
                                    default:
                                        break;
                                }
                                row[fieldName + "Value"] = oVal;
                                row["ColumnId"] = column.Id;
                                row["DataFrameId"] = dataFrameId;
                                row["RowIndex"] = rowIdx;
                                dt.Rows.Add(row);
                                #endregion
                                if(oVal == DBNull.Value)
                                {
                                    stats.Errors.Add(new ErrorInfo(rowIdx, column, "Conversion error. From " + csv[i], LogLevels.WARN));
                                }
                            }
                            catch (Exception ex)
                            {
                                stats.Errors.Add(new ErrorInfo(rowIdx, column, ex.Message, LogLevels.WARN));
                                Logger.Log(LogLevels.WARN, rowIdx, ex.Message, column);
                            }
                        }
                    }
                    rowIdx++;
                }
                stats.SrcRowsCount = rowIdx;
                return dt;
            }
        }
        
        public static ImportStatsInfo ImportCsv(string fileName, SchemaInfo schema, DbContext db)
        {
            var stats = new ImportStatsInfo();
            var df = new DataFrameInfo();
            df.Schema = schema;
            df.Username = Environment.UserName;
            df = db.SaveDataFrame(df);

            stats.DataFrameId = df.Id;
            try
            {
                var dt = LoadCsv(fileName, schema, df.Id, -1, stats);
                db.BulkCopy(dt);
            }
            catch (LoaderException lex)
            {
                Logger.Log(LogLevels.WARN, -1, lex.ToString(), null);
            }

            return stats;
        }
    }
}
